----------Greeting Card Maker----------
A 4am crack                  2015-04-26
-------------------. updated 2023-10-08
                   |___________________

Name: Greeting Card Maker
Genre: graphics
Year: 1986
Credits:
  Produced by Michael Feinberg
  Art Director: Hilary Mills
  Art: Steve Snyder, Karen Elliott,
       Barbara McCosh, Louisa Leary,
       Sheila Lucas, Phil Cassel
  Verse: Paula Polley and Richard Pels
Publisher: Activision, Inc.
Media: double-sided 5.25-inch floppy
OS: DOS 3.3
Other versions: none (preserved here
  for the first time)
Similar cracks: Paper Models: The
  Christmas Kit (4am crack no. 227)

                   ~

               Chapter 0
 In Which Various Automated Tools Fail
          In Interesting Ways


COPYA
  no errors, but copy hangs after
  selecting "(1) Greeting Card Maker"
  from the main menu

Locksmith Fast Disk Backup
  ditto

EDD 4 bit copy (no sync, no count)
  ditto

Copy ][+ nibble editor
  nothing suspicious

Disk Fixer
  looks like unmodified DOS 3.3
  T11 -> regular disk catalog
  T01,S09 -> startup program is "HELLO"

Why didn't my copies work?
  Probably a nibble check called by the
  startup program. Activision loves
  "invisible" nibble checks that don't
  fail immediately but have a side
  effect.

To verify my intuition that the DOS is
unmodified (and the copy protection is
self-contained somewhere else), I'll
boot from a DOS 3.3 master disk and try
to run the program.

[S6,D1=original disk]
[S5,D1=DOS 3.3 master disk]

]PR#5
...
]RUN HELLO,S6
...works...

[S6,D1=non-working copy]
[S5,D1=DOS 3.3 master disk]

]PR#5
...
]RUN HELLO,S6
...fails identically to booting the
non-working copy directly...

Next steps:

  1. Trace the HELLO program
  2. Find the nibble check and document
     any side effects
  3. Disable the nibble check while
     maintaining the side effects

                   ~

               Chapter 1
 In Which It All Starts So Innocently


[S6,D1=original disk]

]PR#6
<Ctrl-C>

]CATALOG

DISK VOLUME 254

*A 009 HELLO
*B 005 TB
*B 002 SE
*B 015 TR
*B 013 LRG HAWTHRN
*B 013 LRG FUTURA
*B 013 SAPHIRE
*B 013 OLD ENGLISH
*B 013 MED TIMES
*B 004 SMALL FONT
*B 013 MED STENCIL
*B 013 TC
*B 005 IF
*B 002 WC
*B 004 TA
*B 015 IP
*B 028 PP
*B 003 PF
*B 003 TD
*B 003 BL
*B 002 CC
 B 002 PS
*B 002 FONTS 1    /FNT
*B 002 VALENTINE  EMBRACE    /VRS
*A 039 ADBK
*B 033 TITLE PAGE
*B 002 FONTS 2    /FNT
*B 002 CZ
*B 003 ADDR.OBJ
*B 016 CLOWN      /PT2
*B 010 ROSE       /PT0
*B 028 GA
*B 017 GB
*B 017 GC
*B 010 GD
*B 021 GE
*B 002 PATTERNS 1 /PAT
*B 003 PLACEMENT 1/GRP
*B 004 FANCY      /BRD
*B 003 HEART      /OBT
*B 003 PLACEMENT 2/GRP
*B 003 BIRTHDAY   /DES
*B 002 PATTERNS 2 /PAT
*B 009 BABY DUCK  /PT1
*B 012 DEER       /PT3
*B 002 CHRISTMAS  MERRY      /VRS
*B 003 CHRISTMAS 1/DES
*B 003 FLOWER     /OBT
*B 026 RABBIT     /SCN
*B 002 HANAKKAH   HAPPY      /VRS
*B 002 MOTHERS DAYCLEAR      /VRS
*B 002 FATHERS DAYCLEAR      /VRS
*B 002 HALLOWEEN  SCARY      /VRS
*B 002 BIRTHDAY   THINKING   /VRS
 B 002 PROGRAM SETUP
*B 002 WEDDING    BEST       /VRS
*B 002 GRADUATION STEP ONE   /VRS
*B 002 NEW BABY   LITTLE ONE /VRS
*B 002 GET WELL   TOP        /VRS
*B 002 SYMPATHY   CONDOLENCES/VRS
*B 002 GENERAL    THINKING   /VRS
*B 002 NEW YEAR   NEW HOPES  /VRS

]LOAD HELLO
]LIST

 10  ONERR  GOTO 10
 20  PRINT "NOMON C,I,O"
 30  HOME
 40  PRINT "MAXFILES 1"
 50  POKE 49286,1
.
. loads TITLE PAGE and displays it
.
 155  PRINT "BLOAD PROGRAM SETUP,
     A$93C4"
 160  PRINT "BLOAD GE"
 170  PRINT "BLOAD GC"
.
. [prints credits and waits]
.
 420  PRINT "BLOAD GB"
 430  HOME
 440  HTAB 10: VTAB 4
 450  PRINT "Which would you like
     ?"
 460  HTAB 1: VTAB 7
 470  PRINT : PRINT "(1) Greeting
      Card Maker"
 480  PRINT : PRINT "(2) Address
     Book/Envelope Maker"
 490  PRINT : PRINT "(3) Setup Pr
     ogram Interface"
 500  HTAB 31: VTAB 4
 510  POKE  - 16368,0
 520  GET A$
 530  IF A$ = "1" THEN 1030
.
.
.
 1030  PRINT
 1040  PRINT "BRUN GA"

My non-working copy got as far as
displaying that main menu at line 430,
but selecting "1" accessed the disk and
froze. So I'll need to trace into the
"GA" binary.

                   ~

               Chapter 2
 In Which It All Goes Wrong So Quickly


]BLOAD GA
]CALL -151

*AA72.AA73   ; last BLOAD address

AA72- 00 08

*800L

0800-   4C 00 20    JMP   $2000

*2000L

; set reset vector to The Badlands
; (presumably)
2000-   A9 0E       LDA   #$0E
2002-   8D F2 03    STA   $03F2
2005-   A9 22       LDA   #$22
2007-   8D F3 03    STA   $03F3
200A-   49 A5       EOR   #$A5
200C-   8D F4 03    STA   $03F4

; counters? or an address?
200F-   A9 11       LDA   #$11
2011-   85 FA       STA   $FA
2013-   A9 22       LDA   #$22
2015-   85 FB       STA   $FB

; suspicious
2017-   A9 C5       LDA   #$C5
2019-   48          PHA

; memory move
201A-   A9 00       LDA   #$00
201C-   85 FC       STA   $FC
201E-   A2 03       LDX   #$03
2020-   BC 3D 20    LDY   $203D,X

; ah, it was an address
2023-   91 FA       STA   ($FA),Y
2025-   CA          DEX
2026-   10 F8       BPL   $2020

; suspicious
2028-   8A          TXA
2029-   48          PHA

We've now pushed $C5/$FF to the stack,
so an RTS right now would jump to $C600
and reboot slot 6. Let's try to avoid
that.

202A-   20 41 20    JSR   $2041

*2041L

; call RWTS to position drive head
2041-   A5 FB       LDA   $FB
2043-   A4 FA       LDY   $FA
2045-   20 B5 B7    JSR   $B7B5
2048-   A9 00       LDA   #$00
204A-   85 48       STA   $48
204C-   90 02       BCC   $2050

; on error, pop the real return address
; and leave $C5/$FF at the top of the
; stack, then RTS to reboot
204E-   68          PLA
204F-   68          PLA
2050-   60          RTS

My non-working copy didn't reboot, so I
guess it got past this.

Continuing at $202D...

*202DL

; get slot number (x16) into X
202D-   A0 01       LDY   #$01
202F-   8C 1D 22    STY   $221D
2032-   B1 FA       LDA   ($FA),Y
2034-   AA          TAX

; here we go
2035-   20 51 20    JSR   $2051

We're closing in on the copy protection
routine. Can you feel it? I swear I can
feel it. The random PHA instructions
are a good clue. Disk seeks for no
apparent reason. The no-second-chances
approach to error handling. This is
unfriendly territory.

                   ~

               Chapter 3
        In Which We Forge Into
         Unfriendly Territory


*2051L

; turn on drive motor manually
; (literally never not suspicious)
2051-   BD 89 C0    LDA   $C089,X

; initialize death counters
2054-   A9 56       LDA   #$56
2056-   85 FD       STA   $FD
2058-   A9 08       LDA   #$08
205A-   C6 FC       DEC   $FC
205C-   D0 04       BNE   $2062
205E-   C6 FD       DEC   $FD

; if death counter hits 0, branch
2060-   F0 34       BEQ   $2096

; look for a specific nibble ($FB)
2062-   BC 8C C0    LDY   $C08C,X
2065-   10 FB       BPL   $2062
2067-   C0 FB       CPY   #$FB
2069-   D0 ED       BNE   $2058
206B-   F0 00       BEQ   $206D

; kill a few cycles (not pointless,
; because the disk spins independently
; of the CPU, so all of these low-level
; disk reads are highly time-sensitive)
206D-   EA          NOP
206E-   EA          NOP

; read data latch (note: no BPL loop
; here, we're just reading it once)
206F-   BC 8C C0    LDY   $C08C,X

; do a compare to set or clear the
; carry bit (among other things, but
; it's the carry bit we care about)
2072-   C0 08       CPY   #$08

; rotate the carry into the low bit of
; the accumulator
2074-   2A          ROL

; if we just rolled a "1" bit out of
; the high bit of the accumulator, take
; this branch
2075-   B0 0B       BCS   $2082

; next nibble needs to be $FF
2077-   BC 8C C0    LDY   $C08C,X
207A-   10 FB       BPL   $2077

; ...otherwise we start over
207C-   C0 FF       CPY   #$FF
207E-   D0 D8       BNE   $2058

; loop back to get next nibble
2080-   F0 EB       BEQ   $206D

; execution continues here (from $2075)
; get another nibble
2082-   BC 8C C0    LDY   $C08C,X
2085-   10 FB       BPL   $2082

; stash it in zero page
2087-   84 FC       STY   $FC

; if the accumulator is anything but
; %00001010, start over
2089-   C9 0A       CMP   #$0A
208B-   D0 CB       BNE   $2058

I got lost several times trying to
follow this routine. I think the
easiest way to explain it is to show
the difference between the original
disk and my non-working copy.

Here is the original disk, as seen by
the Copy II+ nibble editor. Nibbles
with extra "0" bits (timing bits) after
them are displayed in inverse on an
original machine, marked here with a
"+" after the nibble.

                 --v--

   COPY ][ PLUS BIT COPY PROGRAM 8.4
(C) 1982-9 CENTRAL POINT SOFTWARE, INC.
---------------------------------------

TRACK:     START: 1B1E  LENGTH: 17C1

1C70: 9F EB E5 FC D7 D7 D7 EE   VIEW
1C78: FA E6 E6 FF FE F2 ED FD
1C80: FF EF ED BA BB DD AF E6
1C88: B7 A7 CB B7 DE AA EB FF
1C90: FF FF FF FB+FF FF+FF FF+
1C98: FD FF+FF+FF+FF+FF+FF+FF+
1CA0: FF+FF+D5 AA 96 AA AB AA
1CA8: AA AA AB AA AA DE AA EB+
1CB0: FF+FF+FF+FF+FF+FF D5 AA
---------------------------------------

  A  TO ANALYZE DATA  ESC TO QUIT

  ?  FOR HELP SCREEN  /  CHANGE PARMS

  Q  FOR NEXT TRACK   SPACE TO RE-READ

                 --^--

It's easy to understand why a simple
sector copy failed. The sequence that
this code is looking for starts at
offset $1C93, which is between the end
of one sector and the beginning of the
next. (The data epilogue is at $1C8C;
the next address prologue is at $1CA2.)
Sector copiers discard everything
between those delimiters and rebuild
the track with a default pattern of
sync bytes. That pattern doesn't
include an $FB nibble, so the nibble
check fails.

But the EDD bit copy also failed. Here
is the original disk's pattern at
offset $1C93:

  - $FB + timing bit
  - $FF
  - $FF + timing bit
  - $FF
  - $FF + timing bit

And here is what the same part of the
track looks like on my failed EDD copy:

                 --v--

   COPY ][ PLUS BIT COPY PROGRAM 8.4
(C) 1982-9 CENTRAL POINT SOFTWARE, INC.
---------------------------------------

TRACK:     START: 1B1E  LENGTH: 17C1

1C70: 9F EB E5 FC D7 D7 D7 EE   VIEW
1C78: FA E6 E6 FF FE F2 ED FD
1C80: FF EF ED BA BB DD AF E6
1C88: B7 A7 CB B7 DE AA EB FF
1C90: FF FF FF FB+FF FF FF+FF+
1C98: FD FF+FF+FF+FF+FF+FF+FF+
1CA0: FF+FF+D5 AA 96 AA AB AA
1CA8: AA AA AB AA AA DE AA EB+
1CB0: FF+FF+FF+FF+FF+FF D5 AA
---------------------------------------

  A  TO ANALYZE DATA  ESC TO QUIT

  ?  FOR HELP SCREEN  /  CHANGE PARMS

  Q  FOR NEXT TRACK   SPACE TO RE-READ

                 --^--

A subtle difference! The sequence at
offset $1C93 now looks like this:

  - $FB + timing bit
  - $FF
  - $FF
  - $FF + timing bit
  - $FF + timing bit

This code is looking for $FF bytes with
an alternating pattern of timing bit,
no timing bit, timing bit, no timing
bit. It doesn't find that on the bit
copy, so it knows it's not running on
an original disk.

Continuing the code listing...

; get a nibble
208D-   BD 8C C0    LDA   $C08C,X
2090-   10 FB       BPL   $208D

; more bit twiddling
2092-   38          SEC
2093-   2A          ROL

; AND it with the previously stashed
; nibble
2094-   25 FC       AND   $FC

; Success path falls through to here,
; but the failure path is also here
; (from $2060, when the death counters
; hit zero). In other words, we will
; always end up here regardless of
; whether the nibble check "passed,"
; but the value in the accumulator will
; be wrong if the nibble check failed.
2096-   49 AA       EOR   #$AA

; The difference between the original
; disk and my non-working copy is right
; here, in the value stored in $20C0.
; It's not used right away, but I'll
; bet you anything that it will be used
; soon.
2098-   8D C0 20    STA   $20C0

; "This nibble check will self-destruct
; in ten seconds..."
209B-   A9 00       LDA   #$00
209D-   A8          TAY
209E-   99 41 20    STA   $2041,Y
20A1-   C8          INY
20A2-   C0 5D       CPY   #$5D
20A4-   D0 F8       BNE   $209E
20A6-   60          RTS

And that's it. Whether the nibble check
succeeds or fails, this routine returns
to the caller. It doesn't even set a
flag. The only difference is the value
of $20C0.

Continuing from $2038 (the first
instruction after the JSR to $2051)...

*2038L

; pop $C5/$FF off the stack
2038-   68          PLA
2039-   68          PLA

; continue elsewhere
203A-   4C A7 20    JMP   $20A7

*20A7L

; a memory move
20A7-   A9 0F       LDA   #$0F
20A9-   85 FA       STA   $FA
20AB-   A9 20       LDA   #$20
20AD-   85 FB       STA   $FB
20AF-   A9 A7       LDA   #$A7
20B1-   85 FC       STA   $FC
20B3-   A9 20       LDA   #$20
20B5-   85 FD       STA   $FD
20B7-   20 F6 21    JSR   $21F6

; another memory move
20BA-   20 25 22    JSR   $2225

; continue elsewhere
20BD-   4C CF 20    JMP   $20CF

*20CFL

20CF-   AD CC 20    LDA   $20CC
20D2-   18          CLC
20D3-   6D C0 20    ADC   $20C0
20D6-   85 02       STA   $02
20D8-   AD CD 20    LDA   $20CD
20DB-   38          SEC
20DC-   ED C0 20    SBC   $20C0
20DF-   85 03       STA   $03
20E1-   AD CE 20    LDA   $20CE
20E4-   4D C0 20    EOR   $20C0
20E7-   85 04       STA   $04

There you go: three operations that
rely on the correct value in $20C0.

Luckily, this code is not obfuscated or
even difficult to patch. I can do it
right now without rebooting.

*203A:4C 59 FF   ; instead of JMP $20A7

*2000G

*20C0

20C0- 55

The correct value is $55. Now I can
hard-code that value on disk.

[Disk Fixer]
  --> "D" for directory mode
    --> select file "GA"
      --> arrows to follow the binary
          (it starts at $0800, so press
          right arrow 24 times to get
          to the sector that loads at
          $2000)

T20,S09,$C4 change "00" to "55"

And neutralize the nibble check
subroutine at $2051 by changing the
first instruction to an "RTS":

T20,S09,$55 change "BD" to "60"

Quod erat liberandum.

                   ~

               Changelog


2023-10-08

- add missing side B

2015-09-06

- Vastly improved explanation of the
  actual protection routine. Thanks to
  qkumba for pointing out that my
  original explanation was inaccurate.

2015-04-26

- initial release

---------------------------------------
A 4am crack                     No. 295
------------------EOF------------------
